//////////////////////////////////////////////////////////////////
//
//   FDJS Libary
//   (c) Kerry Clendinning, 2007
//
//   FDJS is a simple library that adds a functional declarative
//   mechanism to Javascript.
//
//   Licensed under the Mozilla Public License 1.1
//   http://www.mozilla.org/MPL/
//
//   This header must remain intact.
//
//   Project homepage http://code.google.com/p/fdjs/
//
//////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////
//
//   Class FDJSValue
//
//   This is the FDJSValue base class.  Instantiate a few 
//   of these objects and use their formula parameter to 
//   establish declarative relationships on a web page.
//
//   Usage:
//       obj = new FDJSValue ( object, 
//                             attribute,
//                             formula, 
//                             events )
//
//   parameters:
//          object - (object)  the form element
//       attribute - (string)  attribute of the form element to publish
//         formula - (closure) declarative formula
//          events - (string)  comma-separated list of events to cause
//                             value to update and propagate
//
//    the closure may alternatively be an FDJSValue object
//
//////////////////////////////////////////////////////////////////

function FDJSValue( object, attribute, formula, eventlist ) {

   this.dependTracker.remember( this )
   this.checkRecurse = 0
   this.fdjsItemId = FDJSValue.prototype.fdjsItemNextVal++
   this.eventsAsChange = 0
   this.alwaysNotify = 0
   this.priorValue = null
   this.formulaName = ""

   if ( arguments.length == 1 ) {
      formula = object   // first parameter is just a formula
   }

   if ( typeof formula == 'function' ) {
       this.acceptValue = formula
   } else if ( formula == null ) {
       this.acceptValue = function ( ) { return null }
   } else if ( typeof formula == 'object' ) {
       this.acceptValue = function ( ) { return formula.getValue( ) }
   } else {
       this.acceptValue = function ( ) { return formula }
   }

   this.myDependencies = new Array()

   if ( arguments.length == 1 ) {
      this.parent = this
      this.attrObject = this
      this.id = "anonymous"
   } else {
      if ( typeof object == 'string' ) {
         object = document.getElementById(object)
      }
 
      this.parent = object
      this.attribute = ""

      if ( typeof attribute == 'string' ) {
          this.attrObject = this.parent
          var mylist = attribute.split('.')
          for ( i in mylist ) {
             if ( i < mylist.length - 1) {
                this.attrObject = this.attrObject[ mylist[i] ]
             } else {
                this.attribute = mylist[i]
             }
          }
      } else if ( typeof attribute == 'function' ) {
          this.getter = attribute
      } else {
          if ( attribute [ 'getter' ] ) {
              this.getter = attribute.getter
          } else {
              this.getter = function () { return 0 }
          }
          if ( attribute [ 'setter' ] ) {
              this.setter = attribute.setter
          } else {
              this.setter = function (v) { }
          }
      }
      if ( arguments.length > 3 ) {
          this.setListeners(object, this, eventlist)
      }
   } 

   this.currentValue = this.getValue( )
}

//////////////////////////////////////////////////////////////////
//
// FDJSInherit - function to aid in creating FDJSValue subclass
//
//////////////////////////////////////////////////////////////////

function FDJSInherit( subclass ) {
  var c = function () { }
  c.prototype = FDJSValue.prototype
  subclass.prototype = new c
}

//////////////////////////////////////////////////////////////////
//
// FDJSNotifyValue - an FDJSValue with alwaysNotify set
// already
//
//////////////////////////////////////////////////////////////////

function FDJSNotifyValue ( object, attribute, formula, eventlist ) {
   if ( arguments.length == 1 ) {
       FDJSValue.call ( this, object )
   } else if ( arguments.length == 3 ) {
       FDJSValue.call ( this, object, attribute, formula )
   } else {
       FDJSValue.call ( this, object, attribute, formula, eventlist )
   }
   this.setNotify()
}

FDJSInherit( FDJSNotifyValue )

//////////////////////////////////////////////////////////////////
//
// getValue - used within formula(s) to subscribe
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.getValue = function ( ) { 
  if ( this.dependTracker.intrack == 1) {
      this.setDependency( this.dependTracker.tracking )
      return "0"
  } else {
      if (this.checkRecurse == 1) {
         FDJSValue.prototype.evalAnnounce++
      }
      if ( this.attribute == "" ) {
         return this.getter( )
      } else {
         return this.attrObject[this.attribute]
      }
  }
}

//////////////////////////////////////////////////////////////////
//
// getDependency - subscribe (only)
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.getDependency = function ( ) { 
  if ( this.dependTracker.intrack == 1) {
      this.setDependency( this.dependTracker.tracking )
  }
  return null
}

//////////////////////////////////////////////////////////////////
//
// peekValue - get value of a formula with no subscription
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.peekValue = function ( ) { 
  if ( this.dependTracker.intrack == 1) {
      return "0"
  } else {
      return this.currentValue
//        if ( this.attribute == "" ) {
//           return this.getter( )
//        } else {
//           return this.attrObject[this.attribute]
//        }
  }
}

//////////////////////////////////////////////////////////////////
//
// getPriorValue - get prior value of a formula with subscription
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.getPriorValue = function ( ) { 
  if ( this.dependTracker.intrack == 1) {
      this.setDependency( this.dependTracker.tracking )
      return "0"
  } else {
      return this.priorValue
  }
}

//////////////////////////////////////////////////////////////////
//
// peekPriorValue - get prior value of a formula with subscription
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.peekPriorValue = function ( ) { 
  if ( this.dependTracker.intrack == 1) {
      return "0"
  } else {
      return this.priorValue
  }
}

//////////////////////////////////////////////////////////////////
//
// setFormulaName - sets the name used in the log
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.setFormulaName = function ( n ) { 
   this.formulaName = n
}

//////////////////////////////////////////////////////////////////
//
// setEventChange - set an object so that it always considers
// input events as changed values
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.setEventChange = function ( ) { 
   this.eventsAsChange = 1
}

//////////////////////////////////////////////////////////////////
//
// setNotify - set an object so that it always considers
// input values, including propagated input, as changed
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.setNotify = function ( ) { 
   this.alwaysNotify = 1
}

//////////////////////////////////////////////////////////////////
//
// FDJSExamine ( v )
//
//    v - object | value | closure  to be evaluated
// 
//////////////////////////////////////////////////////////////////

function FDJSExamine ( v ) {
     if ( typeof v == 'function' ) {
         return v()
     } else if ( typeof v == 'object' ) {
         return v.getValue( )
     } else {
         return v
     } 
}

//////////////////////////////////////////////////////////////////
//
// FDJSIf - function to implement a traceable IF operation
//
// usage:
//         FDJSIf( test,
//                 thenbranch,
//                 elsebranch )
//
//   parameters:
//          test - (closure) the "IF" part
//    thenbranch - (closure) the "THEN" part
//    elsebranch - (closure) the "ELSE" part
//
//    each closure may alternatively be an FDJSValue object
//    or a simple value
//
//////////////////////////////////////////////////////////////////

function FDJSIf ( test, thenbranch, elsebranch ) {
   if ( FDJSValue.prototype.dependTracker.intrack == 1) {
     // Just gathering dependencies.  cascade to 
     // any closures in our argument list
     FDJSExamine( test )
     FDJSExamine( thenbranch )
     FDJSExamine( elsebranch )
     return "0"
   } else {
     // Really doing an evaluation.  First get the test result.
     // Deal with closure, object, or value

     var testresult = FDJSExamine( test )

     // Now we know whether to go with THEN or else.  Again,
     // Deal with closure, object, or value for each
     if (testresult) {
         return FDJSExamine( thenbranch )
     } else {
         return FDJSExamine( elsebranch )
     }
   }
}

//////////////////////////////////////////////////////////////////
//
// FDJSWhile
//
// usage:
//         FDJSWhile( test,
//                    body )
//
//   parameters:
//          test - (closure) 
//          body - (closure) 
//
//    executes the body repeatedly while the test is "true"
//
//    either closure may alternatively be an FDJSValue object
//    or a simple value
//
//////////////////////////////////////////////////////////////////

function FDJSWhile ( test, body ) {
   var result;

   if ( FDJSValue.prototype.dependTracker.intrack == 1) {
     // Just gathering dependencies.  cascade to 
     // any closures in our argument list
     FDJSExamine(  test )
     FDJSExamine(  body )
     return "0"
   } else {
     // Really doing an evaluation.  First get the test result.
     // Deal with closure, object, or value

     while ( FDJSExamine(  test ) )  {
         result = FDJSExamine(  body )
     }
   }
   return result;
}

//////////////////////////////////////////////////////////////////
//
// FDJSRecent
//
// returns the value of the formula that is recently changed,
// out of a list of potential formulas
//
// formulas may be closures or objects, but not expressions
//
// any argument may be an array of more arguments, too!
//
//////////////////////////////////////////////////////////////////

function FDJSRecent ( )  {
   return FDJSRecentArray ( arguments )
}

function FDJSRecentArray ( args )  {
   if ( FDJSValue.prototype.dependTracker.intrack == 1) {
     // Just gathering dependencies.  cascade to 
     // any closures in our argument list
     for ( var i = 0; i < args.length; i++ ) {
         if ( args[ i ].length > 0 ) {
            FDJSRecentArray( args [ i ] )
         } else {
            if ( ( typeof args[ i ] != 'object' ) &&
               ( typeof args[ i ] != 'function' ) ) {
                 alert ( typeof args [ i ] )
                 throw("Recent() only works with object and function (closure) args")
               }
            FDJSExamine( args[ i ] )
         }
     }
     return "0"
   } else {
     var i = 0
     var was = FDJSValue.prototype.evalAnnounce
     while ( i < args.length ) {
        if ( args[ i ].length > 0 ) {
           var result = FDJSRecentArray( args [ i ] )
           if ( result != null )
               return result
        }
        var result = FDJSExamine( args[ i ] )
        if ( FDJSValue.prototype.evalAnnounce > was ) {
           return result
        }
        i ++
     }
   }
   return null
}

//////////////////////////////////////////////////////////////////
//
// FDJSDecode
//
// usage:
//         FDJSDecode( value,
//                     test1,
//                     result1
//                     [ ,test2
//                       ,result2 ]
//                     [ , default ]
//                     )
//   Finds the first test that matches value, and returns the
//   corresponding result.  If no test is a match, and a default
//   is provided, it will be the return value.
//
//   parameters:
//
//    each parameter may be a closure, an FDJSValue object
//    or a simple value
//
//////////////////////////////////////////////////////////////////

function FDJSDecode ( )  {
    return FDJSDecodeList ( arguments[0], arguments, 1 )
}

//////////////////////////////////////////////////////////////////
//
// decode () - same as FDJSDecode, above, but as a method
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.decode = function ( ) { 
    var args = FDJSValue.prototype.decode.arguments
    return FDJSDecodeList ( this, args, 0 )
}

//////////////////////////////////////////////////////////////////
//
// FDJSDecodeList - actual implmentation of FDJSDecode and 
// decode() method
//
//////////////////////////////////////////////////////////////////

function FDJSDecodeList ( v, args, offset ) {
   if ( FDJSValue.prototype.dependTracker.intrack == 1) {
     // Just gathering dependencies.  cascade to 
     // any closures in our argument list
     FDJSExamine( v )
     for ( var i = offset; i < args.length; i++ ) {
         FDJSExamine(  args[ i ] )
     }
     return "0"
   } else {
     var testresult = FDJSExamine( v )

     i = offset
     while ( i <= args.length ) {
        if ( i + 1 >= args.length ) {
          return FDJSExamine( args[ i ] )
        }

        if ( testresult == FDJSExamine( args[ i ] ) ) {
           return FDJSExamine( args[ i + 1 ] )
        }

        i += 2
     }
   }
}

//////////////////////////////////////////////////////////////////
//
// acceptor - private method that implements the subscribe
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.acceptor = function ( distance ) {
  if ( this.dependTracker.intrack == 0) {
     if ( this.attribute == "" ) {
        this.setter( this.acceptValue ( ) )
     } else {
        this.attrObject[ this.attribute ] = this.acceptValue ( )
     }
     this.announce( distance )
  }
}

//////////////////////////////////////////////////////////////////
//
// announce - private method that implements the publish
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.announce = function ( distance ) { 
  if (this.checkRecurse == 0) {
      var newval = this.getValue( )
      if ( ( distance == 0 && this.eventsAsChange == 1) || 
           ( this.alwaysNotify == 1) || 
           ( newval != this.currentValue ) ) {
             this.log(this.formatId() + ": " + this.currentValue + " => " + newval)
         this.priorValue = this.currentValue
         this.currentValue = newval
         this.checkRecurse = 1

            for ( d in this.myDependencies ) {
               this.log( "&nbsp;&nbsp;> " + this.myDependencies[d].formatId() )
	         this.myDependencies[d].acceptor( distance + 1 ) 
            }
         this.checkRecurse = 0
      }
  }
}

//////////////////////////////////////////////////////////////////
// 
//   Class FDJSDependTracker  
//
//   A singleton of this class is used by the FDJSValue class
//   to track the dependency lists between values
//
//////////////////////////////////////////////////////////////////

function FDJSDependTracker () {
   this.intrack = 0
   this.dependentList = new Array()

   this.remember = function ( o ) {
      this.dependentList.push( o )
   }

   this.detect = function ( ) {
      this.intrack = 1
      for ( d in this.dependentList ) {
         this.tracking = this.dependentList[d]
         this.dependentList[d].acceptValue( )
      }
      this.intrack = 0
      this.dependentList = new Array()
   }

}

//////////////////////////////////////////////////////////////////
//
//  intialize class variables
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.haveLogger = false  // No logger until one is installed
FDJSValue.prototype.fdjsItemNextVal = 1
FDJSValue.prototype.dependTracker = new FDJSDependTracker;
FDJSValue.prototype.evalAnnounce = 0

//////////////////////////////////////////////////////////////////
//
// formatId - give a brief unique name for this object
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.formatId = function (  ) {
  if ( this.formulaName ) {
    return ( this.formulaName )
  }
  if ( this.parent [ 'id' ] )
    if ( typeof this.attribute == 'string' )
       return ( this.parent.id + "." + this.attribute + " <" + this.fdjsItemId + ">" )
    else
       return ( this.parent.id + " <" + this.fdjsItemId + ">" )
  else
    return ( "<" + this.fdjsItemId + ">" )
}

//////////////////////////////////////////////////////////////////
//
// setDependency - private method to maintain dependencies
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.setDependency = function ( o ) {
  
  this.log( "dependency " + o.formatId() + " <- " + this.formatId() )
  if ( o != this ) {
     this.myDependencies.push( o )
  }
}

//////////////////////////////////////////////////////////////////
//
// wrapMethod - private method to wrap a listener method
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.wrapMethod = function ( oldf, newf ) {
  if (oldf) {
    return function () { var res = oldf()
		         newf(); 
		         return res } 
  } else {
    return function () { newf(); }
  }
}

//////////////////////////////////////////////////////////////////
//
// setListeners - private method to attach an event listener 
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.setListeners = function ( parent, o, eventlist ) {
  var announcefun = function () { o.announce( 0 ) }
  if ( eventlist != "" ) {
     eventitems = eventlist.split(",")
     for (s in eventitems) {
         var oldf = parent [ eventitems[s] ] 
         parent[ eventitems[s] ] = this.wrapMethod(oldf, announcefun)
     }
  }
}

//////////////////////////////////////////////////////////////////
//
// setLogger - provide an html output area for tracking events
//
//////////////////////////////////////////////////////////////////
FDJSValue.prototype.setLogger = function ( o ) {
   FDJSValue.prototype.haveLogger = true

   if ( typeof o == 'string' ) {
       o = document.getElementById( o )
   }

   FDJSValue.prototype.loggerObject = o

   if ( ! o.log ) {
       o.log = function ( t ) { 
             o.innerHTML = o.innerHTML + t + "<BR>"
       }
   }
}

//////////////////////////////////////////////////////////////////
//
// clearLogger - turn of logging
//
//////////////////////////////////////////////////////////////////
FDJSValue.prototype.clearLogger = function ( o ) {
   FDJSValue.prototype.haveLogger = false
}

//////////////////////////////////////////////////////////////////
//
// log - log a message, if we have logging enabled
//
//////////////////////////////////////////////////////////////////

FDJSValue.prototype.log = function (t) {
   if (this.haveLogger) {
        this.loggerObject.log( t )
   }
   return t
}
